10 美团-一面
> Last Format Time:6/12/2026 21:03:57
提问
- hooks使用的注意事项
- content-type的值是什么样的
- React组件的生命周期是怎么样的,如果有父子组件呢
这三个问题涵盖了 React 开发中的核心机制与网络基础,下面为你逐一详细解答:
Hooks 使用的注意事项
在使用 React Hooks 时,为了保证组件状态的正确性和避免诡异的 Bug,必须遵守以下核心原则和常见避坑点:
- 严格遵守两条铁律:
- 只在最顶层使用 Hook:绝对不要在循环、条件判断(
if/else)或者嵌套函数中调用 Hook。React 依靠 Hooks 的调用顺序来维护组件的状态,顺序一旦被打乱,状态就会错乱。 - 只在 React 函数组件或自定义 Hook 中调用:不要在普通的 JavaScript 函数中调用 Hook。
- 只在最顶层使用 Hook:绝对不要在循环、条件判断(
- 闭包陷阱与依赖项数组:
- 在使用
useEffect、useCallback、useMemo时,必须准确填写依赖项数组(deps)。如果漏填依赖,回调函数内部可能会拿到过期的旧状态(闭包陷阱)。 - 如果某些逻辑确实不需要响应依赖变化,应使用
useRef来保存最新值。
- 在使用
- 副作用的清理(Cleanup):
- 在
useEffect中如果开启了定时器、添加了全局事件监听或建立了 WebSocket 连接,必须在返回的清理函数中进行清除(如clearInterval、removeEventListener),否则会导致内存泄漏。
- 在
- React 18 严格模式下的"双次执行":
- 在开发环境下,React 的
<StrictMode>会故意将组件挂载两次(mount -> unmount -> remount),以检测副作用是否纯净。这会导致useEffect执行两次。这是正常现象,生产环境不会发生,切勿为了规避它而写出不规范的代码。
- 在开发环境下,React 的
- 保持 Render 的纯净:
- 不要在组件的渲染主体(即
return之前的逻辑)中直接执行fetch请求、setTimeout或直接修改 DOM,这些副作用必须放在useEffect中。
- 不要在组件的渲染主体(即
Content-Type 的值是什么样的
Content-Type(也称为 MIME 类型)是 HTTP 协议中用于描述"实体主体"数据格式的头部字段,它告诉接收方(浏览器或服务器)如何解析这段数据。
它的标准语法格式为:类型名/子类型名 [; 可选参数]。
以下是开发中最常见的几种 Content-Type 值及其应用场景:
| Content-Type 值 | 数据形态与说明 | 典型应用场景 |
|---|---|---|
| application/json | JSON 格式数据 | 现代前后端 API 交互最常用的数据格式 |
| application/x-www-form-urlencoded | 键值对(如 name=Tom&age=18) | 传统 HTML 表单默认提交格式,不含文件上传 |
| multipart/form-data | 多段二进制数据流 | 文件上传时必须使用此类型,需配合 boundary 参数分隔字段 |
| text/html | HTML 纯文本 | 浏览器返回网页文档,通常会带 charset=utf-8 参数 |
| application/octet-stream | 二进制流 | 触发浏览器下载文件(如 zip、exe),不尝试在浏览器内打开 |
| text/plain | 无格式的纯文本 | 返回未经渲染的普通文本内容 |
| image/jpeg, image/png | 图片二进制数据 | 直接在 <img> 标签中展示图片 |
React 组件的生命周期(含父子组件顺序)
React 组件的生命周期主要分为三个阶段:挂载(Mounting)、更新(Updating) 和 卸载(Unmounting)。
在现代 React(React 16.3+ / React 18)的类组件中,核心生命周期方法的执行顺序如下:
- 挂载阶段:
constructor->static getDerivedStateFromProps->render->componentDidMount - 更新阶段:
static getDerivedStateFromProps->shouldComponentUpdate->render->getSnapshotBeforeUpdate->componentDidUpdate - 卸载阶段:
componentWillUnmount
(注:函数组件通过 useEffect 来模拟这些阶段:无依赖数组相当于挂载/卸载;有依赖数组相当于挂载/更新/卸载。)
父子组件的生命周期执行顺序
当父组件中包含子组件时,生命周期遵循**"父准备 -> 子渲染 -> 子完成 -> 父完成"**的规律。
1. 初始挂载阶段:
- 渲染阶段(由父到子):
- 父组件
constructor - 父组件
getDerivedStateFromProps - 父组件
render(解析出子组件) - 子组件
constructor - 子组件
getDerivedStateFromProps - 子组件
render
- 父组件
- 提交阶段(由子到父,DOM 挂载完成): 7. 子组件
componentDidMount8. 父组件componentDidMount
2. 更新阶段(以父组件 setState 触发更新为例):
- 渲染阶段(由父到子):
- 父组件
getDerivedStateFromProps - 父组件
shouldComponentUpdate - 父组件
render - 子组件
getDerivedStateFromProps - 子组件
shouldComponentUpdate - 子组件
render
- 父组件
- 提交阶段(由子到父,DOM 更新完成): 7. 子组件
getSnapshotBeforeUpdate8. 子组件componentDidUpdate9. 父组件getSnapshotBeforeUpdate10. 父组件componentDidUpdate
3. 卸载阶段:
- 如果父组件被卸载,它会先触发自己的卸载逻辑,然后触发子组件的卸载。
- 核心顺序:父组件
componentWillUnmount-> 子组件componentWillUnmount(注:在实际的 DOM 移除过程中,通常是子组件先被从 DOM 中摘除,但在 React 的生命周期方法调用链中,往往表现为父组件触发卸载流程,进而导致子组件执行卸载钩子)。
代码
实现一个useRequest()
在 React 中封装一个基础的 useRequest 自定义 Hook,可以帮助我们统一管理异步请求的加载状态(loading)、返回数据(data)和错误信息(error)。
下面是一个基于 React 原生 Hooks(useState, useEffect, useCallback, useRef)封装的 useRequest 实现方案:
基础版 useRequest 封装
import { useState, useEffect, useCallback, useRef } from 'react';
// 定义 useRequest 的返回状态类型
interface UseRequestState<T> {
data: T | null;
error: Error | null;
loading: boolean;
}
// 定义 useRequest 的返回结果,包含状态和操作函数
interface UseRequestReturn<T, P extends any[]> extends UseRequestState<T> {
run: (...params: P) => Promise<T | void>;
cancel: () => void;
}
function useRequest<T, P extends any[]>(
service: (...args: P) => Promise<T>,
options: { manual?: boolean } = {}
): UseRequestReturn<T, P> {
const { manual = false } = options;
// 管理请求的状态
const [state, setState] = useState<UseRequestState<T>>({
data: null,
error: null,
loading: !manual, // 如果是自动请求,初始 loading 为 true
});
// 使用 useRef 存储 service,防止闭包陷阱,始终保持最新的 service 引用
const serviceRef = useRef(service);
serviceRef.current = service;
// 用于取消请求的 AbortController
const abortControllerRef = useRef<AbortController | null>(null);
// 核心请求执行函数
const run = useCallback(async (...params: P) => {
// 如果上一次请求还未结束,先取消它
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
const controller = new AbortController();
abortControllerRef.current = controller;
// 开始请求,设置 loading 为 true
setState((prev) => ({ ...prev, loading: true, error: null }));
try {
const result = await serviceRef.current(...params);
// 请求成功,更新 data 和 loading
if (!controller.signal.aborted) {
setState({ data: result, error: null, loading: false });
return result;
}
} catch (error) {
// 如果是主动取消的请求,不更新错误状态
if ((error as any).name === 'AbortError') return;
// 请求失败,更新 error 和 loading
if (!controller.signal.aborted) {
setState({ data: null, error: error as Error, loading: false });
throw error;
}
} finally {
// 清理当前的 controller 引用
if (abortControllerRef.current === controller) {
abortControllerRef.current = null;
}
}
}, []);
// 取消请求的方法
const cancel = useCallback(() => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
abortControllerRef.current = null;
}
setState((prev) => ({ ...prev, loading: false }));
}, []);
// 如果不是手动模式,组件挂载时自动发起请求
useEffect(() => {
if (!manual) {
// 这里假设自动请求不带参数,如果有默认参数可以通过 options 传入
run();
}
// 组件卸载时取消请求
return () => {
cancel();
};
}, [manual, run, cancel]);
return { ...state, run, cancel };
}
export default useRequest;
核心功能解析
- 状态管理 (
useState):统一管理loading、data、error三个核心状态。 - 自动/手动触发 (
options.manual):默认情况下组件挂载会自动执行请求;如果设置manual: true,则需要调用返回的run方法手动触发(例如点击按钮查询)。 - 请求取消 (
AbortController):利用浏览器原生的AbortController实现请求取消。如果在一次请求还未完成时发起了新的请求,或者组件卸载了,会自动取消上一次的请求,防止旧数据覆盖新数据。 - 解决闭包问题 (
useRef):使用useRef存储传入的service异步函数,确保在请求执行时永远拿到的是最新定义的函数引用。
如何在组件中使用
import React from 'react';
import useRequest from './useRequest';
// 模拟一个异步请求函数
const fetchUserInfo = async (id: number) => {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
if (!res.ok) throw new Error('Network response was not ok');
return res.json();
};
const UserInfo = () => {
// 手动触发模式
const { data, loading, error, run } = useRequest(fetchUserInfo, { manual: true });
const handleFetch = () => {
run(1).catch(err => console.log('请求出错:', err));
};
return (
<div>
<button onClick={handleFetch} disabled={loading}>
{loading ? '加载中...' : '获取用户信息'}
</button>
{error && <p style={{ color: 'red' }}>请求失败: {error.message}</p>}
{data && (
<div>
<h3>用户: {data.name}</h3>
<p>邮箱: {data.email}</p>
</div>
)}
</div>
);
};
export default UserInfo;
进阶扩展思路
在实际的生产项目中(如参考业界成熟的 ahooks 库),useRequest 往往还会通过插件化的方式扩展更多高级功能:
- 防抖与节流 (
debounceWait/throttleWait):适用于搜索框等高频触发场景。 - 轮询 (
pollingInterval):定时自动重新发起请求,适用于实时数据监控。 - 缓存与 SWR (
cacheKey):缓存请求结果,提升用户体验。 - 生命周期钩子 (
onSuccess,onError,onFinally):方便在请求的不同阶段处理业务逻辑。
你可以根据实际业务需求,在这个基础版本上逐步添加这些功能。
拼音匹配
实现一个方法,根据拼音返回输入内容,两个参数分别是字符串数组和字符串,返回一个数组,用例如下:
- 输入是['北京BeiJing', '上海ShangHai', '重庆ChongQing', '宝鸡BaoJi'], 'BeiJ', 要求返回['北京']
- 输入是['北京BeiJing', '上海ShangHai', '重庆ChongQing', '宝鸡BaoJi'], 'BJ', 要求返回['北京','宝鸡']
- 输入是['宜昌YiChang', '延安YanAn'], 'YA', 要求返回**['延安', '宜昌']**
const list = [ "北京BeiJing", "上海ShangHai", "重庆ChongQing", "宝鸡BaoJi","宜昌YiChang", "延安YanAn"];
/**
* input: string 待匹配的拼音,如"YA", "BeiJ"
**/
function match(input: string) {
// do something
// 返回城市名称列表
}
const list = [ "北京BeiJing", "上海ShangHai", "重庆ChongQing", "宝鸡BaoJi","宜昌YiChang", "延安YanAn"];
/**
* input: string 待匹配的拼音,如"YA", "BeiJ"
**/
function match(input = "YA") {
if(!input) return []
const l = []
list.forEach((city, index)=>{
let c = city.slice(2)
l.push(c)
})
function getWeight (item) {
let map = new Map()
for(char of item){
map.set(char, (map.has(char) ?? 0) + 1)
}
let weight = 0
for(char of input){
if(map.has(char)) {weight+=map.get(char)}
}
return weight
}
let weight = new Map()
l.forEach((item, index)=>{
weight.set(getWeight(item), item)
})
let res = Array.from(weight)
return res
}
console.log(match("BeiJ"))